package com.globopt.global.algorithm;

import java.util.Random;

/**
 * This class contains the Unirandi method. Unirandi is a local search method.
 * 
 * @author Richard Gasztany 2012.11.29.
 * 
 */
public class Unirandi extends Method {

	private int directionSearchNumber;

	/**
	 * Init
	 * 
	 * @param directionSearchNumber
	 */
	public Unirandi(int directionSearchNumber) {
		this.directionSearchNumber = directionSearchNumber;
	}

	@Override
	public Point loc(Point point) {
		functionEvaluation = 0;
		Point p = point;
		double f = global.getFun().evaluation(p);
		functionEvaluation++;

		for (int i = 0; i < directionSearchNumber; i++) {
			Point direction = direction();
			double h = 1.0;

			while (h > (global.getOpts().getRk() / 8.0)) {

				Point p1 = newPoint(p, direction, global.getOpts().getRk() / 8.0);
				double f1 = global.getFun().evaluation(p1);
				Point p2 = newPoint(p, direction, -global.getOpts().getRk() / 8.0);
				double f2 = global.getFun().evaluation(p2);
				functionEvaluation += 2;

				Point pp = p1;
				double ff = f1;
				if (f2 < f1) {
					pp = p2;
					ff = f2;
				}
				if (ff < f) {
					if (pp == p1) {
						p = linSearch(p, f, direction, h);
					} else {
						p = linSearch(p, f, direction, -h);
					}
					f = global.getFun().evaluation(p);
				}
				h /= 2.0;
			}
		}

		return p;
	}

	/**
	 * Linear searching method. It is looking for a minimum point on a straight
	 * line.
	 * 
	 * @param point
	 * @param f
	 *            calculated function value
	 * @param direction
	 *            searching direction
	 * @param h
	 *            computation accuracy
	 * @return Point
	 */
	private Point linSearch(Point point, double f, Point direction, double h) {
		Point p = point;

		while (true) {
			Point pp = newPoint(p, direction, h);
			double ff = global.getFun().evaluation(pp);
			functionEvaluation++;
			if (f <= ff) {
				return p;
			} else {
				p = pp;
				f = ff;
			}
		}
	}

	/**
	 * Give back a new random point for the direction.
	 * 
	 * @return Point
	 */
	private Point direction() {
		double[] coord = new double[nvars];
		double distance = 0.0;
		Random random = new Random();
		for (int i = 0; i < coord.length; i++) {
			coord[i] = random.nextDouble() - 0.5;
			distance += coord[i] * coord[i];
		}
		distance = Math.sqrt(distance);

		for (int i = 0; i < coord.length; i++) {
			coord[i] /= distance;
		}

		return new Point(coord);
	}

	/**
	 * Add a new Point.
	 * 
	 * @param point
	 * @param direction
	 *            searching direction
	 * @param h
	 *            computation accuracy
	 * @return Point
	 */
	private Point newPoint(Point point, Point direction, double h) {
		double[] coord = new double[nvars];

		for (int i = 0; i < coord.length; i++) {
			coord[i] = point.getCoordinate(i) + h * direction.getCoordinate(i);
			if (coord[i] > global.getUpperBound(i)) {
				coord[i] = global.getUpperBound(i);
			}

			if (coord[i] < global.getLowerBound(i)) {
				coord[i] = global.getLowerBound(i);
			}

		}

		return new Point(coord);
	}
}
